import { Object3D, Mesh } from 'three'

export { type AnimationUniforms, animateAllMaterial }

type AnimationUniforms = {
  /** 模型总高度 */
  height: { value: number }
  /** 标准层高 */
  floorHeight: { value: number }
  /** 底层高 */
  bottomFloorHeight: { value: number }
  /** 底层层数 */
  bottomFloors: { value: number }
  /** 总工期 */
  totalDays: { value: number }
  /** 开始施工的时间 */
  startDay: { value: number }
  /** 当前时间 */
  thisDay: { value: number }
}

/** 为名称带有参数的子元素设置参数材质 */
function animateAllMaterial(
  target: Object3D,
  uniforms: AnimationUniforms,
  cache: AnimationUniforms[] = []
): AnimationUniforms[] {
  if (target.type === 'Mesh') {
    replaceWithAnimatedMaterial(target as Mesh, uniforms)
  }
  target.children.forEach((c) => animateAllMaterial(c, uniforms, cache))
  return cache
}

/** 替换参数材质 */
function replaceWithAnimatedMaterial(target: Mesh, uniforms: AnimationUniforms): void {
  const before = Array.isArray(target.material) ? target.material[0] : target.material
  if (before) {
    const after = before.clone()
    after.onBeforeCompile = (shader) => {
      shader.vertexShader = shader.vertexShader
        .replace(
          'void main() {',
          `
  uniform float height;
  uniform float floorHeight;
  uniform float bottomFloors;
  uniform float bottomFloorHeight;
  uniform float totalDays;
  uniform float startDay;
  uniform float thisDay;
  void main() {`
        )
        .replace(
          '#include <project_vertex>',
          `vec4 mvPosition = vec4( transformed, 1.0 );

        #ifdef USE_BATCHING
          mvPosition = batchingMatrix * mvPosition;
        #endif

        #ifdef USE_INSTANCING
          mvPosition = instanceMatrix * mvPosition;
        #endif

        // 须先计算顶点的世界坐标，并以此进行计算
        mvPosition = modelMatrix * mvPosition;

        // 计算当前限高
        float maxHeight = 0.0;
        float totalFloors = bottomFloors + round((height - bottomFloors * bottomFloorHeight) / floorHeight);
        float floorPerDay = totalFloors / totalDays;
        if (thisDay > startDay) {
          float daysPassed = thisDay - startDay;
          float thisFloors = round(daysPassed * floorPerDay);
          if (thisFloors > bottomFloors) {
              maxHeight = bottomFloors * bottomFloorHeight + (thisFloors - bottomFloors) * floorHeight;
          } else {
              maxHeight = thisFloors * bottomFloorHeight;
          }
        }

        // 默认Y轴朝上，超过限高的设为限高
        if (mvPosition.y > maxHeight) mvPosition.y = maxHeight;

        mvPosition = viewMatrix * mvPosition;
        gl_Position = projectionMatrix * mvPosition;`
        )
      Object.assign(shader.uniforms, uniforms)
    }

    target.material = after
    before.dispose()
  }
}
